# -*- coding: utf-8 -*-


#文件:  MemAllocLog_Analyze.py


## 0. 安装依赖
# pip install pandas
# pip install plotly
# pip install seaborn

import pickle
import os

from pathlib import Path

from matplotlib.axes import Axes
from plotly.graph_objs import Scatter, Figure, Scatter3d

interpreter:str=os.environ['_']
#interpreter取值:
# ipython: '/app/miniconda3/bin/ipython'
# jupyter notebook: '/app/miniconda3/bin/jupyter'
# python : '/app/miniconda3/bin/python3'
interpreterIsJupyter:bool= interpreter.endswith("jupyter")


import numpy
import math

import matplotlib.pyplot as plt


import seaborn

import plotly.express as plotly_express
import plotly.offline as plotly_offline
import plotly.graph_objs as graph_objs
from plotly.graph_objs import XAxis,YAxis,ZAxis

import plotly.figure_factory as figure_factory

#本地终端下运行，不要这句话，否则 plotly_offline.iplot不显示图。 但jupyter notebook下运行，需要这句话。
# plotly_offline.init_notebook_mode(connected=True)

import pandas
pandas.options.display.max_columns = None
pandas.options.display.max_rows = None


##fn=ToReplaceMeByTerminalPython
#默认输入文件. 当在jupyter notebook中 "%load MemAllocLog_Analyze.py" 前 设置输入文件路径变量fn,  则使用该路径.
notHasFnVar:bool='fn' not in locals() and  'fn' not in globals()
print(f"notHasFnVar:{notHasFnVar}")
if notHasFnVar:
    fn="./clang/_MemAllocLog_clang-15_20230526_002306.out.new"
    print(f"using default fn:{fn}")

print(f"fn:{fn}")

Path(f"{Path(fn).parent}/img/").mkdir(exist_ok=True)

#纳秒 、秒 转换
NanoSecondsPerSecond=10**9

#纳秒 、毫秒 转换
NanoSecondsPerMillSecond=10**6

## 1. 加载日志文件到pandas
df=pandas.read_csv(filepath_or_buffer=fn,sep=',')

## 2. 构造常见所需字段
df['memSizeFull']=df['memSize']*df['cnt']
# df['memSizeFull']=df['memSizeFull'].apply(lambda x:math.log(x+1,32))
df['abs_tv_nsec']=NanoSecondsPerSecond*df['timespecCurrent.tv_sec']+df['timespecCurrent.tv_nsec']
df['func_color'] = df['func'].apply(lambda x: ord(x)-ord('f'))
df['func_color'] = df['func_color'].apply(lambda x: 255 - ( int(71**abs(x)-1 ) % 255 )  )

df['memAddr']=df['memAddr'].apply(lambda x:0 if x == '(nil)' else   int(x,base=16)  )

abs_tv_nsec__min=df['abs_tv_nsec'].min()
print(f"abs_tv_nsec__min:{abs_tv_nsec__min}")
df['abs_tv_nsec']=df['abs_tv_nsec'].apply(lambda x: x-abs_tv_nsec__min  )


## 3. 创建  内存地址下标

#memAddr  的具体值差异太大，转为下标

# 真实内存地址memAddr --> 真实内存地址集合 -->  各个真实内存地址 在该集合中的下标 即   memAddrIdx
# 后续 内存地址 用 此下标 memAddrIdx， 这样 最大下标+1 即可以作为 新内存地址，即 方便新内存地址构造
memAddr_ls=df['memAddr'].to_list()
memAddr_ls=list(set(sorted(memAddr_ls)))
#memAddr_ls 即 真实内存地址集合

memAddr2Idx={v:idx for idx,v in enumerate(memAddr_ls)}

#memAddr2Idx   即 真实内存地址memAddr 对应的 下标

#创建 内存地址下标字段 memAddrIdx， 后续用此字段表示内存地址
df['memAddrIdx']=df['memAddr'].apply(lambda x:memAddr2Idx[x])

#memAddr2Idx 值例子:
_memAddr1=memAddr_ls[1]
print(f"{_memAddr1}: {memAddr2Idx[ _memAddr1 ]}")

"""
1. 理想状况如下: (理想尚未实现）
>  (实现此理想需要malloc hook改为全面自实现的内存管理  ： 替代glibc的malloc内存管理， 工作量很大)
>  如果都一个地址 一生 只会被用到一次 ( malloc、calloc、realloc ), 则这里 每条横线段  线段起点 表示 一次分配、线段终点表示释放 ，会很容易分析

------------------

2.  而现在现状是：  (现状即问题)
>    一条横线段上有很多个点， 是因为一个地址 一生 被复用了很多次  ，  
>            这是glibc 内存管理导致的，详见：  https://www.jianshu.com/p/2fedeacfa797
>          一条横线段 以free点 为分割，分割成 多条横线段，  
>                 这多条横线段 每个都是一个独立的 内存分配释放行为 ，这多条横线段之间没有任何关系

-----------------
3.  下面这段解决此问题： 
>  解决过程描述:  将横线按照free点切割 成多个线段，切割后的每个线段给以新内存地址
"""

##  4. 同内存地址的动作序列（一根原始横线段） 按free点切割 成 多个无关小横线段


from typing import Dict


# 产生 左右都包含的区间
def rangeABInclude(a, b):
    return list(range(a, b + 1))


memAddrIdx_ls = list(set(df['memAddrIdx'].to_list()))

memAddrIdxMax = max(memAddrIdx_ls)
print(f"memAddrIdxMax:{memAddrIdxMax}")


# 输入: 一个df , 表示一条原始横线（有多个free点的横线)， 内存地址一样的 所以是一条横线
# 输出: 与输入结构一样的一个df,  但表示原始那一条横线按其上的多个free点切割 后 成的 多条横线，  切割后的各线段内存地址不一样了  所以是多条横线
def dfKProc(dfK):
    # dfK=df[df['memAddrIdx']==2684]
    # dfK=df[df['memAddrIdx']==2364]
    # dfK.head()

    #####

    dfKLen = len(dfK)
    #     print(dfKLen)

    # 这段分组是开发时的尝试，已经不需要了
    #     gpK=dfK .reset_index().groupby('func')['index'].apply(numpy.array)
    #     print(gpK)
    #     kFIndex=gpK['f']
    #     print(kFIndex)
    #     dfK.loc[kFIndex].head()

    #############
    #     print(dfK)
    # 按free点分割
    kIdx: Dict = dfK.reset_index().groupby('func').indices
    kFIdx = kIdx.get('f', None)
    if kFIdx is None:
        return dfK

    #     print(kFIdx)

    #     dfK.iloc[kFIdx].head()

    # 每一个free点 作为一个区间的右界 （显然该区间的左界是上一个free点往后一个位置）
    # 这里的区间 左右都包含7
    left = [0 - 1, *kFIdx]
    right = kFIdx
    segLs = [rangeABInclude(left[i] + 1, r) for i, r in enumerate(right)]
    rightEnd = right[-1]
    # 整体的起点和结尾点 要特殊处理，这里处理结尾点，起点实际上不需要处理，因为起点产生区间[0,0] 这不影响
    if rightEnd < dfKLen - 1:
        segLs = [*segLs, rangeABInclude(rightEnd + 1, dfKLen - 1)]

    #     print(segLs)
    # segLs 就是 构造好的 区间列表
    memAddrIdxNext = memAddrIdxMax
    for seg in segLs[1:]:  # 跳过原始横线的第0线段，即第0线段用原始横线的内存地址即可
        # 后续每根子线段，都需要给新内存地址
        memAddrIdxNext += 1  # 内存地址 已经以下标样式出现，因此下标加1 即是新内存地址
        #     dfK.iloc[seg]['memAddrIdx']=memAddrIdxNext
        dfK.iloc[seg, dfK.columns.get_loc('memAddrIdx')] = memAddrIdxNext
    #     dfK.iloc[seg].assign(memAddr=memAddrIdxNext,inplace=True)

    #     dfK.iloc[segLs[1]]

    #     dfK
    # memAddrIdx列已经被修改的不同了

    #尾free点 的 memSizeFull 改为 前一个分配点(a\c\r点)的 memSizeFull， 以保持同一线段在z轴等高
    memSizeFull_col=dfK.columns.get_loc('memSizeFull')
    for seg in segLs:
        if len(seg) > 1:
            dfK.iloc[ [seg[-1]], memSizeFull_col] = dfK.iloc[  [seg[-2]] , memSizeFull_col]

    #调试用
    """
    if dfKLen>3:
        import pickle,time
        nowNs=time.time_ns()  #1685102537258164346
        pickle.dump(segLs,open(f"/tmp/{nowNs}_segLs","wb"))
        pickle.dump(dfK,open(f"/tmp/{nowNs}_dfK","wb"))
    
    #jupyter notebook 此函数外, 用以下语句复原出此函数中的变量 以供调试用
    #dfK=pickle.load(open("/tmp/1685102766730482227_dfK","rb"))
    #segLs=pickle.load(open("/tmp/1685102766730482227_segLs","rb"))
    """

    return dfK


import numpy
# dfKLs: 保存切割后的线段
dfKLs = []
# 遍历每一个内存地址（注意 内存地址 不要用memAddr ， 应该用 下标样式 memAddrIdx，这样新地址很好产生）
for mAIdxK in memAddrIdx_ls:
    dfK = df[df['memAddrIdx'] == mAIdxK]
    dfK = dfK.sort_values('abs_tv_nsec')
    dfK = dfKProc(dfK)
    dfKLs.append(dfK)

print(len(dfKLs), len(memAddrIdx_ls))


pandas.concat( dfKLs[:2], axis=0).head()


#合并 切割后的线段
df2=pandas.concat( dfKLs, axis=0)
df2=df2.reset_index()

df2.count()
df.count()

df0=df
df=df2
#用df2替换df

df.head(4)

## 5. 显示颜色值
df[df["func"]=='a'] [ ['func', 'func_color'] ] .head(1)
df[df["func"]=='f'] [ ['func', 'func_color'] ] .head(1)
df[df["func"]=='r'] [ ['func', 'func_color'] ] .head(1)
df[df["func"]=='c'] [ ['func', 'func_color'] ] .head(1)

#列举 若干 内存地址下标 值
df['memAddrIdx'].value_counts()[:13]

## 6.保存新csv文件:
fn2=f"{fn}.2"
df.to_csv(fn2)
fn2

##  7. 可以去plotly在线网站可视化 fn2文件
"""
> 至此，可以选择：

> 去  [chart-studio.plotly.com](https://chart-studio.plotly.com/create/#/  )
> 展示 文件 fn2   

1. x轴 : abs_tv_nsec
2. y轴 : memAddr
3. size: memSizeFull
4. color: func_color
"""

##  8. seaborn整体画图（虽然效果不太好）
seabornFig:Axes=seaborn.scatterplot(x="abs_tv_nsec", y="memAddrIdx", hue="func_color",size="memSizeFull",data=df)
seabornFig.set_title(fn)
#本地终端下运行，需要这句话，以正常显示图片。 jupyter notebook下运行，不需要这句话。
plt.show(block=False)

print(max(df['memAddrIdx'].to_list()))

#memAddrIdx值举例:
df[df['memAddrIdx']==45]  [:10]


## 9. 对新横线段们 作图 例子 (不是必须的)
#这段只作为例子，不是必须的

#memAddr打印举例:
df_memAddr45=df[df['memAddrIdx']==45]
df_memAddr811=df[df['memAddrIdx']==811]

memAddr45_trace:Scatter = graph_objs.Scatter(
 x = df_memAddr45.abs_tv_nsec,
 y = df_memAddr45.memAddrIdx,
 showlegend=True,
mode = 'lines+markers',
marker=dict(size=6, color=df_memAddr45.func_color),
line=dict(width=1),
name="memAddr45_trace"
)

memAddr811_trace:Scatter = graph_objs.Scatter(
 x = df_memAddr811.abs_tv_nsec,
 y = df_memAddr811.memAddrIdx,
 showlegend=True,
mode = 'lines+markers',
marker=dict(size=6, color=df_memAddr811.func_color),
line=dict(width=1),
name=f"memAddr811_trace:{fn}"
)


plotly_offline.iplot([memAddr45_trace,memAddr811_trace],show_link=True,auto_play=True)

#ref : https://stackoverflow.com/questions/58335021/plotly-how-to-set-width-to-specific-line

#图片在本地浏览器中显示正常


## 10. 对新横线段们 作图
# 产生 一个内存地址 （一条新横线段） 的作图对象
memAddrIdx_layout_2d = graph_objs.Layout(
    #ref : https://www.kaggle.com/code/mmdatainfo/plotly-visualization
    xaxis=graph_objs.layout.XAxis( title="abs_tv_nsec"  ),
    yaxis=graph_objs.layout.YAxis(  title="memAddrIdx"  ),
    title=f"memAddrIdx:{Path(fn).name}"
)
memAddrIdx_layout_3d = graph_objs.Layout(
    #ref : https://stackoverflow.com/questions/26941135/show-legend-and-label-axes-in-plotly-3d-scatter-plots
    showlegend=True,
    scene=graph_objs.Scene(
        xaxis=XAxis(title="abs_tv_nsec"),
        yaxis=YAxis(title="memAddrIdx"),
        zaxis=ZAxis(title="memSizeFull"),
    ),
    title=f"memAddrIdx:{Path(fn).name}"
)

memAddrIdx_layout=memAddrIdx_layout_3d
def memAddrIdxK_trace(_df,memAddrIdxK):
    df_memAddrK=_df[_df['memAddrIdx']==memAddrIdxK]
    traceK:Scatter3d = graph_objs.Scatter3d(
     x = df_memAddrK.abs_tv_nsec,
     y = df_memAddrK.memAddrIdx,
    z=df_memAddrK.memSizeFull,
     showlegend=True,
    mode = 'lines+markers',
    name=f'memAddr_{memAddrIdxK}',
    marker=dict(size=2, color=df_memAddrK.func_color),
    line=dict(width=1)
    )
    return traceK

trace_ls=list(map(lambda memAddrIdxK: memAddrIdxK_trace(df,memAddrIdxK),  memAddrIdx_ls))
print(f"trace_ls len: {len(trace_ls)}")


if interpreterIsJupyter:
    #这里只展示部分 新横线段，因 全部展示 浏览器受不了。
    # 浏览器中 则部分展示
    fig:Figure=graph_objs.Figure(data=trace_ls[0:900], layout=memAddrIdx_layout)
    fig.show()
    #图片在本地浏览器中显示正常, 但该图片不在.ipynb中 所以git仓库上看不到该图片
else:
    #终端中 则全部展示
    fig:Figure=graph_objs.Figure(data=trace_ls, layout=memAddrIdx_layout)
    plotly_offline.iplot(fig)

fig.write_image(f"{Path(fn).parent}/img/fig.svg", width=1300, height=1300)



## 11.     2动作的 内存地址的  生存时长 前100
"""
>  内存地址的  分配时刻到释放时刻（仅仅2个动作: 先分配后释放) 即 该内存地址的生存时长 降序 排列
>  (忽略 有多个动作的内存地址)
"""
nsLs_grpBy_mAI=df.groupby('memAddrIdx')['abs_tv_nsec'].apply(list)
print("type nsLs_grpBy_mAI :",type(nsLs_grpBy_mAI))
print(nsLs_grpBy_mAI[0:10])

#2个动作(先分配后释放)、2个以上动作 的内存地址 对应的  的生存时长
tsGrpMAIdx=[(memAddrIdx,row) for memAddrIdx,row in nsLs_grpBy_mAI.items() if len(row)>=2]
#仅仅2个动作(先分配后释放) 的内存地址 对应的  的生存时长
ts2GrpMAIdx=[(memAddrIdx,row) for memAddrIdx,row in nsLs_grpBy_mAI.items() if len(row)==2]

print(f"总内存地址 个数:{len(nsLs_grpBy_mAI)} \n ，2个动作(先分配后释放)、2个以上动作 的内存地址 个数：{len(tsGrpMAIdx)} \n , 仅2个动作(先分配后释放) 的内存地址 个数： {len(ts2GrpMAIdx)}")

#生存时长 有以下3个选项:
#默认选项A. :  2动作内存地址 生存时长（纳秒数）： 第2个点即尾点free时刻-第1个点即头点时刻(a或c或r点)
mAIdxTs2Delta=[(memAddrIdx,row[1]-row[0]) for memAddrIdx,row  in ts2GrpMAIdx]

#选项B. :  >=2 动作内存地址 生成时长 （纳秒数）  ： 尾点free时刻-倒数第2个点时刻(a或c或r点)
# mAIdxTs2Delta=[(memAddrIdx,row[-1]-row[-2]) for memAddrIdx,row  in tsGrpMAIdx]

#选项C. : >=2 动作内存地址 生成时长 （纳秒数）  ： 尾点free时刻-第1个点时刻(a或c或r点)
# mAIdxTs2Delta=[(memAddrIdx,row[-1]-row[0]) for memAddrIdx,row  in tsGrpMAIdx]

#2动作内存地址 生存时长（纳秒数） 降序排列
mAIdxSortByTs2Delta=sorted(mAIdxTs2Delta,key=lambda x:x[1],reverse=True)


#2动作内存地址 生存时长（纳秒数） 前100
# 浏览器中 则部分展示,  终端中 则全部展示
mAIdxTop100Ts2Delta=mAIdxSortByTs2Delta[:1000] if interpreterIsJupyter else mAIdxSortByTs2Delta
mAIdxTop100=[memAddrIdx for memAddrIdx,ts2Delta  in mAIdxTop100Ts2Delta]
print(mAIdxTop100[:10])
dfTop100Ts2Delta=df[df['memAddrIdx'] .isin(mAIdxTop100)]

#2动作内存地址 生存时长（纳秒数） 前100 ： 作图
traceLs_Top100Ts2Delta=list(map(lambda memAddrIdxK: memAddrIdxK_trace(dfTop100Ts2Delta,memAddrIdxK),  memAddrIdx_ls))
print(f"traceLs_Top100Ts2Delta len: {len(traceLs_Top100Ts2Delta)}")

_title:str="同一内存地址横线段按free点切割为多个不同内存地址小线段后"
if interpreterIsJupyter:
    # 浏览器中 则部分展示
    figTopNTs2Delta:Figure = graph_objs.Figure(data=traceLs_Top100Ts2Delta[0:900], layout=memAddrIdx_layout)
    figTopNTs2Delta.layout.title=f"{figTopNTs2Delta.layout.title.text}:\n{_title}"
    figTopNTs2Delta.show()
    #图片在本地浏览器中显示正常, 但该图片不在.ipynb中 所以git仓库上看不到该图片
else:
    # 终端中 则全部展示
    figTopNTs2Delta:Figure = graph_objs.Figure(data=traceLs_Top100Ts2Delta, layout=memAddrIdx_layout)
    figTopNTs2Delta.layout.title=f"{figTopNTs2Delta.layout.title.text}:\n{_title}"
    plotly_offline.iplot(figTopNTs2Delta)

figTopNTs2Delta.write_image(f"{Path(fn).parent}/img/figTopNTs2Delta.svg",width=1300,height=1300)

# 结果df写pickle， 方便不用整体运行以看结果
pickle.dump(df, open(f"{fn}.df.pickle", "wb"))

if not interpreterIsJupyter:
    input("停在这里，以确保seaborn作图窗口不退出，可以按回车退出")